<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>10.1.3-双端比较的原理</title>
</head>

<body>
    <div id="app">
        <!-- <input value="foo" id="my-input" type="text" value="foo" class="foo" onchange="change()" /> -->
    </div>
</body>
    <script src="https://unpkg.com/@vue/reactivity@3.0.5/dist/reactivity.global.js"></script>
    <script>
        /**
         * 第二轮
         * 
         * 操作前 
         * - 旧 DOM 节点顺序为 =>  p-4、(p-1、p-2、p-3)
         * - 新 DOM 节点顺序为 =>  p-4、(p-2、p-1、p-3)
         * 
         * 进行下一轮的比较 （while循环）
         * - 在第二步时找到了相同的节点
         * - 两者的 key 值相同，可以复用
         * - 由于两者都处于尾部，因此不需要对真实 DOM 进行移动操作，只需要打补丁即可
         * 
         * 此时，
         * - 真实 DOM 节点顺序为 p-4、p-1、p-2、p-3
         * - idxs：
         *    oldStartIdx: 0
         *    oldEndIdx: 1
         *    newStartIdx: 1
         *    newEndIdx: 2
         * */ 

        // 文本节点的 type 标识
        const Text = Symbol();
        const Comment = Symbol();
        const Fragment = Symbol();

        
         function createRenderer(options) {
            // 通过 options 得到操作 DOM 的 API
            const {
                createElement,
                insert,
                setElementText,
                patchProps,
                createText,
                setText,
                createComment, 
            } = options

            // 在这个作用域内定义的函数都可以访问那些 API
            /**
             * 在这个作用域内定义的函数都可以访问那些 API
             * @param {VNode} vnode 虚拟 DOM 对象
             * @param {HTMLElement} container 容器
             * @param {HTMLElement} anchor 锚点元素
             * */ 
            function mountElement(vnode, container, anchor) {
                // 调用 createElement 函数创建元素
                // 让 vnode.el 引用真实 DOM 元素
                const el = vnode.el = createElement(vnode.type)
                if (typeof vnode.children === 'string') {
                    // 调用 setElementText 设置元素的文本节点
                    setElementText(el, vnode.children)
                }else if (Array.isArray(vnode.children)) {
                    // 如果 children 是数组，则遍历每一个子节点，并调用 patch 函数挂载它们
                    vnode.children.forEach(child => {
                        patch(null, child, el)
                    })
                }

                // 如果 vnode.props 存在才处理它
                if (vnode.props) {
                    // 遍历 vnode.props
                    for (const key in vnode.props) {
                        // 调用 patchProps 函数即可
                        patchProps(el, key, null, vnode.props[key])
                    }
                }

                // 调用 insert 函数将元素插入到容器内
                insert(el, container)
            }

            function patchElement(n1, n2) {
                const el = n2.el = n1.el
                const oldProps = n1.props
                const newProps = n2.props
                // 第一步：更新 props
                for (const key in newProps) {
                    if (newProps[key] !== oldProps[key]) {
                        patchProps(el, key, oldProps[key], newProps[key])
                    }
                }
                for (const key in oldProps) {
                    if (!(key in newProps)) {
                        patchProps(el, key, oldProps[key], null)
                    }
                }
                // 第二步：更新 children
                patchChildren(n1, n2, el)
            }
            
            /**
             * @param {VNode} n1 旧的 vnode
             * @param {VNode} n2 新的 vnode
             * @param {HTMLElement} container 容器
             * */ 
            function patchChildren(n1, n2, container) {
                // 判断新子节点的类型是否是文本节点
                if (typeof n2.children === 'string') {
                    // 旧子节点的类型有三种可能：没有子节点、文本子节点以及一组子节点
                    // 只有当旧子节点为一组子节点时，才需要逐个卸载，其他情况下什么都不需要做
                    if (Array.isArray(n1.children)) {
                        n1.children.forEach((c) => unmount(c))
                    }
                    // 最后将新的文本节点内容设置给容器元素
                    setElementText(container, n2.children)
                }else if (Array.isArray(n2.children)) {
                    // 说明新子节点是一组子节点
                    // 判断旧子节点是否也是一组子节点
                    if (Array.isArray(n1.children)) {
                        // 代码运行到这里，说明新旧子节点都是一组子节点
                        // 封装 patchKeyedChildren 函数处理两组子节点
                        patchKeyedChildren(n1, n2, container)
                    } else {
                        // 此时：
                        // 旧子节点要么是文本子节点，要么不存在
                        // 但无论哪种情况，我们都只需要将容器清空，然后将新的一组子节点逐个挂载
                        setElementText(container, '')
                        n2.children.forEach(c => patch(null, c, container))
                    }
                }else{
                    // 代码运行到这里，说明新子节点不存在
                    // 旧子节点是一组子节点，只需逐个卸载即可
                    if (Array.isArray(n1.children)) {
                        console.log('null unmount')
                        n1.children.forEach(c => unmount(c))
                    } else if (typeof n1.children === 'string') {
                        // 旧子节点是文本子节点，清空内容即可
                        setElementText(container, '')
                    }
                    // 如果也没有旧子节点，那么什么都不需要做
                }


                function patchKeyedChildren(n1, n2, container){
                    console.log('patchKeyedChildren run')
                    const oldChildren = n1.children
                    const newChildren = n2.children
                    // 四个索引值
                    let oldStartIdx = 0
                    let oldEndIdx = oldChildren.length - 1
                    let newStartIdx = 0
                    let newEndIdx = newChildren.length - 1
                    // 四个索引指向的 vnode 节点
                    let oldStartVNode = oldChildren[oldStartIdx]
                    let oldEndVNode = oldChildren[oldEndIdx]
                    let newStartVNode = newChildren[newStartIdx]
                    let newEndVNode = newChildren[newEndIdx]

                    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
                        if (oldStartVNode.key === newStartVNode.key){
                            // 第一步：oldStartVNode 和 newStartVNode 比较
                        } else if (oldEndVNode.key === newEndVNode.key) {
                            // 第二步：oldEndVNode 和 newEndVNode 比较
                            // 新增 以下代码
                            // 节点在新的顺序中仍然处于尾部，不需要移动，但仍需打补丁
                            patch(oldEndVNode, newEndVNode, container)
                            // 更新索引和头尾部节点变量
                            oldEndVNode = oldChildren[--oldEndIdx]
                            newEndVNode = newChildren[--newEndIdx]
                            break // 阻止死循环
                        } else if (oldStartVNode.key === newEndVNode.key) {
                            // 第三步：oldStartVNode 和 newEndVNode 比较
                        } else if (oldEndVNode.key === newStartVNode.key) {
                            // 第四步：oldEndVNode 和 newStartVNode 比较
                            // 仍然需要调用 patch 函数进行打补丁
                            patch(oldEndVNode, newStartVNode, container)
                            // 移动 DOM 操作
                            // oldEndVNode.el 移动到 oldStartVNode.el 前面
                            insert(oldEndVNode.el, container, oldStartVNode.el)

                            // 移动 DOM 完成后，更新索引值，并指向下一个位置
                            oldEndVNode = oldChildren[--oldEndIdx]
                            newStartVNode = newChildren[++newStartIdx]
                        }

                    }

                }
            }




            // 第一个参数 n1 代表旧 vnode，第二个参数 n2 代表新 vnode。
            /**
             * 第一个参数 n1 代表旧 vnode，第二个参数 n2 代表新 vnode。
             * @param {VNode} n1 旧的 vnode
             * @param {VNode} n2 新的 vnode
             * @param {HTMLElement} container 容器
             * @param {HTMLElement} anchor 锚点元素
             * */ 
            function patch(n1, n2, container, anchor) {
                 // 如果 n1 存在，则对比 n1 和 n2 的类型
                 if (n1 && n1.type !== n2.type) {
                    // 如果新旧 vnode 的类型不同，则直接将旧 vnode 卸载
                    unmount(n1)
                    n1 = null
                }

                // 代码运行到这里，证明 n1 和 n2 所描述的内容相同
                const { type } = n2
                // 如果 n2.type 的值是字符串类型，则它描述的是普通标签元素
                if (typeof type === 'string') {
                    // 如果 n1 不存在，意味着挂载，则调用 mountElement 函数完成挂载
                    if (!n1) {
                        mountElement(n2, container, anchor)
                    } else {
                        patchElement(n1, n2)
                    }
                } else if (type === Text){
                    // 如果新 vnode 的类型是 Text，则说明该 vnode 描述的是文本节点

                    // 如果没有旧节点，则进行挂载
                    if (!n1) {
                        // 调用 createText 函数创建文本节点
                        const el = n2.el = createText(n2.children);
                        // 将文本节点插入到容器中
                        insert(el, container)
                    } else {
                        // 如果旧 vnode 存在，只需要使用新文本节点的文本内容更新旧文本节点即可
                        const el = n2.el = n1.el
                        if (n2.children !== n1.children) {
                            // 调用 setText 函数更新文本节点的内容
                            setText(el, n2.children)
                        }
                    }

                } else if (type === Comment){
                    if (!n1) {
                        const el = n2.el = createComment(n2.children);
                        insert(el, container)
                    } else {
                        const el = n2.el = n1.el
                        if (n2.children !== n1.children) {
                            setText(el, n2.children)
                        }
                    }


                } else if (type === Fragment){
                     // 处理 Fragment 类型的 vnode
                    if (!n1) {
                        // 如果旧 vnode 不存在，则只需要将 Fragment 的 children 逐个挂载即可
                        n2.children.forEach(c => patch(null, c, container))
                    } else {
                        // 如果旧 vnode 存在，则只需要更新 Fragment 的 children 即可
                        patchChildren(n1, n2, container)
                    }

                } else if (typeof type === 'object') {
                    // 如果 n2.type 的值的类型是对象，则它描述的是组件
                } else if (type === 'xxx') {
                    // 处理其他类型的 vnode
                }
            }

            function render(vnode, container) {
                if (vnode) {
                    // 新 vnode 存在，将其与旧 vnode 一起传递给 patch 函数，进行打补丁
                    patch(container._vnode, vnode, container)
                } else {
                    if (container._vnode) {
                        // 旧 vnode 存在，且新 vnode 不存在，说明是卸载（unmount）操作
                        // 调用 unmount 函数卸载 vnode
                        unmount(container._vnode)
                    }
                }
                // 把 vnode 存储到 container._vnode 下，即后续渲染中的旧 vnode
                container._vnode = vnode
            }

            // 卸载操作
            function unmount(vnode) {
                // 在卸载时，如果卸载的 vnode 类型为 Fragment，则需要卸载其 children
                if (vnode.type === Fragment) {
                    vnode.children.forEach(c => unmount(c))
                    return
                }

                const parent = vnode.el.parentNode
                if (parent) {
                    parent.removeChild(vnode.el)
                }
            }

            return {
                render
            }
        }

        // 创建一个渲染器
        // 在创建 renderer 时传入配置项
        const renderer = createRenderer({
            // 用于创建元素
            createElement(tag) {
                return document.createElement(tag)
            },
            // 用于设置元素的文本节点
            setElementText(el, text) {
                el.textContent = text
            },
            /**
             * 用于在给定的 parent 下添加指定元素
             * @param {HTMLElement} el  用于插入的节点
             * @param {HTMLElement} parent  新插入节点的父节点
             * @param {HTMLElement} anchor  将要插在这个节点之前
             * */ 
            insert(el, parent, anchor = null) {
                parent.insertBefore(el, anchor)
            },
            // 将属性设置相关操作封装到 patchProps 函数中，并作为渲染器选项传递
            patchProps(el, key, prevValue, nextValue) {
                if (/^on/.test(key)) {
                    // 匹配以 on 开头的属性，视其为事件

                    // 定义 el._vei 为一个对象，存在事件名称到事件处理函数的映射
                    const invokers = el._vei || (el._vei = {})
                    //根据事件名称获取 invoker
                    let invoker = invokers[key]
                    // 根据属性名称得到对应的事件名称，例如 onClick ---> click
                    const name = key.slice(2).toLowerCase()
                    if(nextValue){
                        if (!invoker) {
                            // 如果没有 invoker，则将一个伪造的 invoker 处理函数缓存到 el._vei[key] 下，避免覆盖
                            // vei 是 vue event invoker 的首字母缩写
                            invoker = el._vei[key] = (e) => {
                                // e.timeStamp 是事件发生的时间
                                // 如果事件发生的时间早于事件处理函数绑定的时间，则不执行事件处理函数
                                if (e.timeStamp < invoker.attached) return;
                                // 如果 invoker.value 是数组，则遍历它并逐个调用事件处理函数
                                if (Array.isArray(invoker.value)) {
                                    invoker.value.forEach(fn => fn(e))
                                } else {
                                    // 否则直接作为函数调用
                                    // 当伪造的事件处理函数执行时，会执行真正的事件处理函数
                                    invoker.value(e)
                                }
                            }
                            // 将真正的事件处理函数赋值给 invoker.value
                            invoker.value = nextValue
                            // 添加 invoker.attached 属性，存储事件处理函数被绑定的时间
                            invoker.attached = performance.now();
                            // 绑定 invoker 作为事件处理函数
                            el.addEventListener(name, invoker)
                        } else {
                            // 如果 invoker 存在，意味着更新，并且只需要更新 invoker.value 的值即可
                            invoker.value = nextValue
                        }
                    } else if (invoker) {
                        // 新的事件绑定函数不存在，且之前绑定的 invoker 存在，则移除绑定
                        el.removeEventListener(name, invoker)
                    }
                } else if (key === 'class') {
                    // 对 class 进行特殊处理
                    el.className = nextValue || ''
                } else if (shouldSetAsProps(el, key, nextValue)) {
                    // 获取该 DOM Properties 的类型
                    const type = typeof el[key]
                    // 如果是布尔类型，并且 value 是空字符串，则将值矫正为 true
                    if (type === 'boolean' && nextValue === '') {
                        el[key] = true
                    } else {
                        el[key] = nextValue
                    }
                } else {
                    el.setAttribute(key, nextValue)
                }

                // 判断是否只读，如果是，则使用 setAttribute 函数设置属性
                function shouldSetAsProps(el, key, value) {
                    // 特殊处理
                    if (key === 'form' && el.tagName === 'INPUT') return false
                    // 兜底
                    // 用 in 操作符判断 key 是否存在对应的 DOM Properties
                    return key in el
                }
            },
            createText(text) {
                return document.createTextNode(text)
            },
            setText(el, text) {
                el.nodeValue = text
            },
            createComment(text) {
                return document.createComment(text)
            },
        })


        // 新增 测试
        const oldVnode = {
            type: 'div',
            children: [
                { type: 'p', children: '1', key: 1 },
                { type: 'p', children: '2', key: 2 },
                { type: 'p', children: '3', key: 3 },
                { type: 'p', children: '4', key: 4 },
            ]
        }
        const newVnode = {
            type: 'div',
            children: [
                { type: 'p', children: '44', key: 4 },
                { type: 'p', children: '22', key: 2 },
                { type: 'p', children: '11', key: 1 },
                { type: 'p', children: '33', key: 3 },
            ]
        }
        
        renderer.render(oldVnode, document.querySelector('#app'))

        setTimeout(() => {
            renderer.render(newVnode, document.querySelector('#app'))
        }, 2000)

    </script>
    

</html>